using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Xml;
using MathNet.Numerics.LinearAlgebra.Double;
using System.Diagnostics;

namespace MTS
{
    /// <summary>
    /// Sample class
    /// </summary>
    public class Sample
    {
        /// <summary>
        /// Sample data
        /// </summary>
        public DenseVector Data;

        /// <summary>
        /// Default constructor
        /// </summary>
        public Sample()
        {
            Data = null;
        }

        /// <summary>
        /// Constructor that sets number of variables
        /// </summary>
        /// <param name="varCount">Number of items in the sample</param>
        public Sample(int varCount)
        {
            Data = new DenseVector(varCount);
        }

        /// <summary>
        /// Adds new variable at end of sample; sets to 0
        /// </summary>
        public void AddVariable()
        {
            if (Data == null)
            {
                Data = new DenseVector(1);
            }
            else
            {
                Data = (DenseVector)Data.ToRowMatrix().Append(new DenseMatrix(1)).Row(0);
            }
        }

        /// <summary>
        /// Adds new variable at end of sample
        /// </summary>
        /// <param name="value">Value of new variable</param>
        public void AddVariable(double value)
        {
            AddVariable();
            Data[Data.Count - 1] = value;
        }

        /// <summary>
        /// Removes variable from sample
        /// </summary>
        /// <param name="index"></param>
        /// <returns>True on success, else false</returns>
        public bool RemoveVariable(int index)
        {
            if (index < 0 || index >= Data.Count)
                return false;

            List<double> temp = new List<double>();
            for (int i = 0; i < Data.Count; i++)
            {
                if (i != index) temp.Add(Data[i]);
            }

            Data = DenseVector.OfEnumerable(temp);
            return true;
        }

        /// <summary>
        /// Removes multiple variables from sample, using <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="indeces"></param>
        /// <returns></returns>
        public int RemoveVariables(List<int> indeces)
        {
            DenseVector temp = Data;
            indeces.Sort();
            for (int i = indeces.Count - 1; i >= 0; i--)
            {
                if (!RemoveVariable(indeces[i]))
                {
                    Data = temp;
                    return i;
                }
            }
            return -1;
        }
    }

    /// <summary>
    /// Sample class that contains variable names and associated methods
    /// </summary>
    public class SampleXML : Sample
    {
        /// <summary>
        /// List of variable names
        /// </summary>
        private List<string> VariableNames;

        /// <summary>
        /// Default constructor
        /// </summary>
        public SampleXML()
            : base()
        {
            VariableNames = new List<string>();
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="varCount">Number of items in the sample</param>
        public SampleXML(int varCount) : base(varCount)
        {
            VariableNames = new List<string>();
            for (int i = 0; i < varCount; i++)
            {
                VariableNames.Add("");
            }
        }

        /// <summary>
        /// Retrieves the index of the specified variable name.
        /// </summary>
        /// <param name="variableName">Name of the variable</param>
        /// <returns>Zero based index of the specified variable name, returns -1 if not found</returns>
        public int GetVariableIndex(string variableName)
        {
            for (int i = 0; i < VariableNames.Count; i++)
            {
                if (variableName.ToUpper().Trim() == VariableNames[i].ToUpper().Trim())
                    return i;
            }
            return -1;
        }

        /// <summary>
        /// Sets a variable's name for a column within the matrices and arrays.
        /// </summary>
        /// <param name="index"></param>
        /// <param name="name"></param>
        public bool SetVariableName(int index, string name)
        {
            if (index < 0 || index >= Data.Count)
                return false;

            VariableNames[index] = name;
            return true;
        }

        /// <summary>
        /// Retrieve the name of the variable for the specified position in the matrices and arrays.
        /// </summary>
        /// <param name="index">Column index number, zero based.</param>
        /// <returns>Returns empty string if invalid index</returns>
        public string GetVariableName(int index)
        {
            if (index < 0 || index >= VariableNames.Count)
                return "";

            return VariableNames[index];
        }

        /// <summary>
        /// set value of sample variable by variable name
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="value"></param>
        public bool SetValue(string variableName, double value)
        {
            int position = GetVariableIndex(variableName);
            if (position == -1)
                return false;

            Data[position] = value;
            return true;
        }

        /// <summary>
        /// Get value of sample variable by variable name
        /// </summary>
        /// <param name="variableName"></param>
        /// <returns>Value, or Double.NaN on invalid variable name</returns>
        public double GetValue(string variableName)
        {
            int position = GetVariableIndex(variableName);
            if (position == -1)
                return Double.NaN;

            return Data[position];
        }

        /// <summary>
        /// Adds new variable at end of sample; sets to 0
        /// </summary>
        new public void AddVariable()
        {
            base.AddVariable();
            VariableNames.Add("");
        }

        /// <summary>
        /// Adds new variable at end of sample
        /// </summary>
        /// <param name="value">Value of new variable</param>
        new public void AddVariable(double value)
        {
            AddVariable();
            Data[Data.Count - 1] = value;
        }

        /// <summary>
        /// Adds new variable at end of sample; sets to 0
        /// </summary>
        /// <param name="variableName">Name of new variable</param>
        public void AddVariable(string variableName)
        {
            AddVariable();
            VariableNames[VariableNames.Count - 1] = variableName;
        }

        /// <summary>
        /// Adds new variable at ends of sample
        /// </summary>
        /// <param name="variableName">Name of new variable</param>
        /// <param name="value">Value of new variable</param>
        public void AddVariable(string variableName, double value)
        {
            AddVariable(variableName);
            Data[Data.Count - 1] = value;
        }

        /// <summary>
        /// Remove variable from sample
        /// </summary>
        /// <param name="index"></param>
        /// <returns>True on success, else false</returns>
        new public bool RemoveVariable(int index)
        {
            if (index > 0 && index < VariableNames.Count)
            {
                if (base.RemoveVariable(index))
                {
                    VariableNames.RemoveAt(index);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Remove variable from sample, using <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="variableName"></param>
        /// <returns>True on success, else false</returns>
        public bool RemoveVariable(string variableName)
        {
            int index = GetVariableIndex(variableName);
            if (index >= 0)
                return RemoveVariable(index);
            return false;
        }

        /// <summary>
        /// Removes multiple variables from sample, using <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="indeces"></param>
        /// <returns>-1 on success, else index of variable causing failure</returns>
        new public int RemoveVariables(List<int> indeces)
        {
            indeces.Sort();
            DenseVector oldData = Data;
            List<string> oldVarNames = VariableNames;
            for (int i = indeces.Count - 1; i >= 0; i--)
            {
                if (!RemoveVariable(indeces[i]))
                {
                    Data = oldData;
                    VariableNames = oldVarNames;
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Removes multiple variables from sample, using <see cref="RemoveVariable(int)"/> and <see cref="GetVariableIndex(string)"/>
        /// </summary>
        /// <param name="variableNames"></param>
        /// <returns></returns>
        public int RemoveVariables(List<string> variableNames)
        {
            List<int> indeces = new List<int>();
            for (int i = 0; i < variableNames.Count; i++)
            {
                int index = GetVariableIndex(variableNames[i]);
                if (index == -1)
                {
                    indeces.Add(index);
                }
                else
                {
                    return index;
                }
            }
            return RemoveVariables(indeces);
        }
    }

    /// <summary>
    /// TargetSpace class for Mahalanobis space
    /// </summary>
    public class TargetSpace
    {
        /// <summary>
        /// Mahalanobis space data
        /// </summary>
        public DenseMatrix Data;

        /// <summary>
        /// Default constructor. Sets Data = null
        /// </summary>
        public TargetSpace()
        {
            Data = null;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="size">size = sample count = variable count</param>
        public TargetSpace(int size)
        {
            Data = new DenseMatrix(size);
            Debug.WriteLine("Warning: MTS requires at least N+1 samples for N variables. Please add at least 1 sample before use.");
        }
        
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="sampleCount">Number of samples</param>
        /// <param name="variableCount">Number of variables</param>
        public TargetSpace(int sampleCount, int variableCount)
        {
            Data = new DenseMatrix(sampleCount, variableCount);
            Debug.WriteLineIf(sampleCount <= variableCount, "Warning: MTS requires at least N+1 samples for N variables. Please add at least " + (variableCount - sampleCount + 1) + " sample(s) before use.");
        }

        /// <summary>
        /// Retrieves a sample from the data matrix.
        /// </summary>
        /// <param name="sampleNum"></param>
        /// <returns>Sample on success, else null</returns>
        public Sample GetSample(int sampleNum) 
        {
            // create a new sample of the requested type
            Sample sample = new Sample(Data.ColumnCount);

            if (sampleNum < 0 || sampleNum >= Data.RowCount)
            {
                return null;
            }
            sample.Data.SetValues(Data.Row(sampleNum).ToArray());

            return sample;
        }
        
        /// <summary>
        /// Adds a sample to the space
        /// </summary>
        /// <param name="newSample"></param>
        /// <returns>True on success, else null</returns>
        public bool AddSample(Sample newSample)
        {
            if (newSample != null)
            {
                if (Data == null) // if empty matrix
                {
                    Data = (DenseMatrix)newSample.Data.ToRowMatrix(); // set Data to sample data
                    return true;
                }
                else if (Data.ColumnCount == newSample.Data.Count) // ensure same variable count
                {
                    Data = (DenseMatrix)Data.InsertRow(0, newSample.Data);
                    return true;
                }
                else return false;
            }
            return false;
        }

        /// <summary>
        /// Removes a sample (row) from the space
        /// </summary>
        /// <param name="index"></param>
        /// <returns>True on success, else false</returns>
        public bool RemoveSample(int index)
        {
            if (index < 0 || index >= Data.RowCount)
            {
                return false;
            }
            else if (index == 0)
            {
                Data = (DenseMatrix)Data.SubMatrix(1, Data.RowCount - 1, 0, Data.ColumnCount);
            }
            else if (index == Data.ColumnCount - 1)
            {
                Data = (DenseMatrix)Data.SubMatrix(0, Data.RowCount - 1, 0, Data.ColumnCount);
            }
            else
            {
                Data = (DenseMatrix)Data.SubMatrix(0, index, 0, Data.ColumnCount).Stack(Data.SubMatrix(index + 1, Data.RowCount - index - 1, 0, Data.ColumnCount));
            }
            return true;
        }

        /// <summary>
        /// Removes multiple samples from space, using <see cref="RemoveSample(int)"/>
        /// </summary>
        /// <param name="indeces"></param>
        /// <returns>-1 on success, else index of sample causing failure</returns>
        public bool RemoveSamples(List<int> indeces)
        {
            indeces.Sort();
            for (int i = indeces.Count - 1; i >= 0; i--)
            {
                if (!RemoveSample(indeces[i])) return false;
            }
            return true;
        }

        /// <summary>
        /// Adds new variable (column) to end of space
        /// </summary>
        public void AddVariable()
        {
            if (Data == null)
            {
                Data = new DenseMatrix(1, 1);
            }
            else
            {
                Data = (DenseMatrix)Data.Append(new DenseMatrix(Data.RowCount, 1));
            }
        }

        /// <summary>
        /// Removes variable (column) from space
        /// </summary>
        /// <param name="index"></param>
        /// <returns>True on success, else false</returns>
        public bool RemoveVariable(int index)
        {
            if (index < 0 || index >= Data.ColumnCount) // if index is out of bounds
            {
                return false;
            }
            else if (index == 0)
            {
                Data = (DenseMatrix)Data.SubMatrix(0, Data.RowCount, 1, Data.ColumnCount - 1);
            }
            else if (index == Data.ColumnCount - 1)
            {
                Data = (DenseMatrix)Data.SubMatrix(0, Data.RowCount, 0, Data.ColumnCount - 1);
            }
            else
            {
                Data = (DenseMatrix)Data.SubMatrix(0, Data.RowCount, 0, index).Append(Data.SubMatrix(0, Data.RowCount, index + 1, Data.ColumnCount - index - 1));
            }
            return true;
        }

        /// <summary>
        /// Removes multiple variables from space, using <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="indeces"></param>
        /// <returns>-1 on success, else index of variable causing failure</returns>
        public int RemoveVariables(List<int> indeces)
        {
            indeces.Sort();
            DenseMatrix oldData = Data;
            for (int i = indeces.Count - 1; i >= 0; i--)
            {
                if (!RemoveVariable(indeces[i]))
                {
                    Data = oldData;
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Returns the correlation matrix
        /// </summary>
        /// <returns>Correlation Matrix as a DenseMatrix</returns>
        public DenseMatrix GetCorrelationMatrix()
        {
            if (Data.RowCount != 0 && Data.ColumnCount != 0)
            {
                DenseMatrix temp = new DenseMatrix(Data.ColumnCount);
                for (int i = 0; i < Data.ColumnCount; i++)
                {
                    for (int j = 0; j < Data.ColumnCount; j++)
                    {
                        temp[i, j] = MathNet.Numerics.Statistics.Correlation.Pearson(Data.Column(i), Data.Column(j));
                    }
                }
                return temp;
            }
            return null;
        }

        /// <summary>
        /// Returns means of each data column
        /// </summary>
        /// <returns>Means as DenseVector</returns>
        public DenseVector GetMeans()
        {
            DenseVector means = new DenseVector(Data.ColumnCount);
            for (int i = 0; i < Data.ColumnCount; i++)
            {
                means[i] = Data.Column(i).Average();
            }
            return means;
        }

        /// <summary>
        /// Returns means of each data column
        /// </summary>
        /// <returns>Means as DenseVector</returns>
        public DenseVector GetStdDevs()
        {
            DenseVector stdDevs = new DenseVector(Data.ColumnCount);
            DenseVector means = GetMeans();
            for (int i = 0; i < Data.RowCount; i++)
            {
                for (int j = 0; j < Data.ColumnCount; j++)
                {
                    stdDevs[j] += (Data[i, j] - means[j]) * (Data[i, j] - means[j]) / Data.RowCount; // could be RowCount or RowCount - 1
                }
            }
            for (int i = 0; i < stdDevs.Count; i++)
            {
                stdDevs[i] = Math.Sqrt(stdDevs[i]);
            }
            return stdDevs;
        }
    }

    /// <summary>
    /// Abstract TargetSpace that provides for file access and reference to variables by name
    /// </summary>
    public abstract class TargetSpaceFile : TargetSpace
    {
        /// <summary>
        /// File name with path
        /// </summary>
        protected string FileName;

        /// <summary>
        /// List of variable names
        /// </summary>
        protected List<string> VariableNames;

        /// <summary>
        /// Default constructor
        /// </summary>
        public TargetSpaceFile() : base() { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="size">size = sample count = variable count</param>
        public TargetSpaceFile(int size) : base(size)
        {
            VariableNames = new List<string>(size);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="sampleCount">Number of samples</param>
        /// <param name="variableCount">Number of variables</param>
        public TargetSpaceFile(int sampleCount, int variableCount) : base(sampleCount, variableCount)
        {
            VariableNames = new List<string>(variableCount);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="fileName">File name with path</param>
        public TargetSpaceFile(string fileName)
        {
            VariableNames = new List<string>();
            FileName = fileName;
            Open();
        }

        /// <summary>
        /// Sets the filename for the target space xml
        /// </summary>
        /// <param name="filename">filename</param>
        public void SetFileName(string filename)
        {
            FileName = filename;
        }

        /// <summary>
        /// Sets a variable's name for a column within the matrices and arrays.
        /// </summary>
        /// <param name="varPosition">Variable position.</param>
        /// <param name="name">Variable name.</param>
        public bool SetVariableName(int varPosition, string name)
        {
            if (varPosition < 0 || varPosition >= VariableNames.Count)
                return false;

            VariableNames[varPosition] = name;
            return true;
        }

        /// <summary>
        /// Retrieve the name of the variable for the specified position in the matrices and arrays.
        /// </summary>
        /// <param name="varPosition">Column index number, zero based.</param>
        /// <returns>Returns empty string if unsuccessful</returns>
        public string GetVariableName(int varPosition)
        {
            if (varPosition < 0 || varPosition >= VariableNames.Count)
                return "";

            return VariableNames[varPosition];
        }

        /// <summary>
        /// Retrieves the index of the specified variable name.
        /// </summary>
        /// <param name="variableName">Name of the variable</param>
        /// <returns>Zero based index of the specified variable name, returns -1 if not found</returns>
        public int GetVariableIndex(string variableName)
        {
            for (int i = 0; i < VariableNames.Count; i++)
            {
                if (variableName.ToUpper().Trim() == VariableNames[i].ToUpper().Trim())
                    return i;
            }
            return -1;
        }


        /// <summary>
        /// Retrieves a sample from the data matrix.
        /// </summary>
        /// <param name="sampleNum"></param>
        /// <returns>Sample on success, else null</returns>
        new public SampleXML GetSample(int sampleNum)
        {
            if (sampleNum < 0 || sampleNum >= Data.RowCount)
            {
                return null;
            }

            // create a new sample
            SampleXML sample = new SampleXML(VariableNames.Count);
            for (int i = 0; i < sample.Data.Count; i++)
            {
                sample.SetVariableName(i, VariableNames[i]);
            }
            sample.Data.SetValues(Data.Row(sampleNum).ToArray());

            return sample;
        }

        /// <summary>
        /// Adds variable (column) to space; sets variable name = ""
        /// </summary>
        new public void AddVariable()
        {
            base.AddVariable();
            VariableNames.Add("");
        }

        /// <summary>
        /// Adds variable (column) to space
        /// </summary>
        /// <param name="variableName">Name of variable</param>
        public void AddVariable(string variableName)
        {
            AddVariable();
            VariableNames[VariableNames.Count - 1] = variableName;
        }

        /// <summary>
        /// Removes variable (column) from space
        /// </summary>
        /// <param name="index"></param>
        /// <returns>True on success; else false</returns>
        new public bool RemoveVariable(int index)
        {
            if (index > 0 && index < VariableNames.Count)
            {
                if (base.RemoveVariable(index))
                {
                    VariableNames.RemoveAt(index);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Removes variable (column) by variable name, using <see cref="GetVariableIndex(string)"/> and <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="variableName">Name of variable</param>
        /// <returns>True on success, else false</returns>
        public bool RemoveVariable(string variableName)
        {
            int index = GetVariableIndex(variableName);
            if (index >= 0 && index < Data.ColumnCount)
            {
                return RemoveVariable(index);
            }
            else return false;
        }

        /// <summary>
        /// Removes variables (columns) by index
        /// </summary>
        /// <param name="indeces">List of indeces to remove</param>
        /// <returns>-1 on success; else index of error</returns>
        new public int RemoveVariables(List<int> indeces)
        {
            indeces.Sort();
            DenseMatrix oldData = Data;
            List<string> oldVarNames = VariableNames;
            for (int i = indeces.Count - 1; i >= 0; i--)
            {
                if (!RemoveVariable(indeces[i]))
                {
                    Data = oldData;
                    VariableNames = oldVarNames;
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Removes variable by variable name, using <see cref="GetVariableIndex(string)"/> and <see cref="RemoveVariable(int)"/>
        /// </summary>
        /// <param name="variableNames">Name of variables</param>
        /// <returns>-1 on success, else index of error</returns>
        public int RemoveVariables(List<string> variableNames)
        {
            List<int> indeces = new List<int>();
            for (int i = 0; i < variableNames.Count; i++)
            {
                int index = GetVariableIndex(variableNames[i]);
                if (index == -1)
                {
                    indeces.Add(index);
                }
                else
                {
                    return index;
                }
            }
            return RemoveVariables(indeces);
        }

        /// <summary>
        /// Provides data source access using <see cref="Open(string)"/>
        /// </summary>
        /// <returns>True on success, else false</returns>
        abstract public bool Open();

        /// <summary>
        /// Provides data source access
        /// </summary>
        /// <param name="fileName">File name with path</param>
        /// <returns>True on success, else false</returns>
        abstract public bool Open(string fileName);

        /// <summary>
        /// Saves space data using <see cref="Save(string)"/>
        /// </summary>
        /// <returns>True on success, else false</returns>
        abstract public bool Save();

        /// <summary>
        /// Save space data
        /// </summary>
        /// <param name="fileName">File name with path</param>
        /// <returns></returns>
        abstract public bool Save(string fileName);
    }

    /// <summary>
    /// TargetSpaceFile implementation that provides XML functionality
    /// </summary>
    public class TargetSpaceXML : TargetSpaceFile
    {
        /// <summary>
        /// Default constructor
        /// </summary>
        public TargetSpaceXML() : base() { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="size">size = sample count = variable count</param>
        public TargetSpaceXML(int size) : base(size) { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="sampleCount">Number of samples</param>
        /// <param name="variableCount">Number of variables</param>
        public TargetSpaceXML(int sampleCount, int variableCount) : base(sampleCount, variableCount) { }

        /// <summary>
        /// Constructor, populates data using XML file
        /// </summary>
        /// <param name="fileName">File name with path</param>
        public TargetSpaceXML(string fileName) : base(fileName) { }

        /// <summary>
        /// Opens XML file with target space data
        /// </summary>
        /// <returns>True if successful, else false</returns>
        public override bool Open()
        {
            try
            {
                VariableNames = new List<string>();
                var xml = new XmlDocument();
                xml.Load(FileName);

                //variable names
                XmlNodeList nodes = xml.DocumentElement.SelectNodes("//TargetSpace//VariableDefinition//Variable");
                foreach (XmlNode node in nodes)
                {
                    //variable names
                    string attr = node.Attributes["name"].InnerText;
                    VariableNames.Add(attr);
                }

                //samples
                nodes = xml.DocumentElement.SelectNodes("//TargetSpace//SampleData//Sample");
                foreach (XmlNode node in nodes)
                {
                    int colCount = 0;
                    SampleXML sample = new SampleXML(node.ChildNodes.Count);
                    foreach (XmlNode n in node.ChildNodes)
                    {
                        sample.Data[colCount++] = Convert.ToDouble(n.InnerText);
                    }
                    if (!AddSample(sample))
                    {
                        return false;
                    }
                }
                return true;
            }
            catch (System.Xml.XmlException e)
            {
                Debug.WriteLine(e.Message);
                return false;
            }
            catch (IOException e)
            {
                Debug.WriteLine(e.Message);
                return false;
            }
        }

        /// <summary>
        /// Open XML file with target space data
        /// </summary>
        /// <param name="fileName">Name of file (include path)</param>
        /// <returns>True on success, else fail</returns>
        public override bool Open(string fileName)
        {
            FileName = fileName;
            return Open();
        }
        
        /// <summary>
        /// Saves the TargetSpace as an XML file.
        /// </summary>
        public override bool Save()
        {
            string contents = "<TargetSpace><VariableDefinitions>";
            for (int i = 0; i < VariableNames.Count; i++)
            {
                contents += "<Variable name=\"" + VariableNames[i] + "\"/>";
            }
            contents += "</VariableDefinitions><SampleData>";
            for (int i = 0; i < Data.RowCount; i++)
            {
                contents += "<Sample>";
                for (int j = 0; j < Data.ColumnCount; j++)
                {
                    contents += "<Value>" + Data[i, j] + "</Value>";
                }
                contents += "</Sample>";
            }
            contents += "</SampleData></TargetSpace>";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(contents);
            try
            {
                doc.Save(FileName);
                return true;
            }
            catch (XmlException e)
            {
                Debug.WriteLine(e.Message);
                return false;
            }
            catch (IOException e)
            {
                Debug.WriteLine(e.Message);
                return false;
            }
        }

        /// <summary>
        /// Saves the TargetSpace as an XML file.
        /// </summary>
        /// <param name="fileName">Name of file with path</param>
        /// <returns>True on success, else fail</returns>
        public override bool Save(string fileName)
        {
            FileName = fileName;
            return Save();
        }
    }

    /// <summary>
    /// TargetSpaceFile implementation that provides CSV functionality
    /// </summary>
    public class TargetSpaceCSV : TargetSpaceFile
    {
        /// <summary>
        /// Default constructor
        /// </summary>
        public TargetSpaceCSV() : base() { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="size">size = sample count = variable count</param>
        public TargetSpaceCSV(int size) : base(size) { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="sampleCount">Number of samples</param>
        /// <param name="variableCount">Number of variables</param>
        public TargetSpaceCSV(int sampleCount, int variableCount) : base(sampleCount, variableCount) { }

        /// <summary>
        /// Constructor, populates data using XML file
        /// </summary>
        /// <param name="fileName">File name with path</param>
        public TargetSpaceCSV(string fileName) : base(fileName) { }

        /// <summary>
        /// Opens and imports the passed in .csv file
        /// </summary>
        /// <returns>True if successful</returns>
        public override bool Open()
        {
            using (StreamReader reader = new StreamReader(File.OpenRead(FileName)))
            {
                try
                {
                    var line = reader.ReadLine();
                    var names = line.Split(',');
                    VariableNames = new List<string>();

                    for (int x = 0; x < names.Length; x++)
                    {
                        VariableNames.Add(names[x]);
                    }

                    //data set
                    while (!reader.EndOfStream)
                    {
                        line = reader.ReadLine();
                        var values = line.Split(',');
                        Sample sample = new Sample(values.Length);

                        for (int y = 0; y < values.Length; y++)
                        {
                            //skip first column for names for each sample
                            //sample.setName(y, variableNames[y]);
                            sample.Data[y] = Convert.ToDouble(values[y]);
                        }
                        if (!AddSample(sample))
                        {
                            reader.Close();
                            return false;
                        }
                    }
                    reader.Close();
                    return true;
                }
                catch (ArgumentException e)
                {
                    Debug.WriteLine(e.Message);
                    return false;
                }
                catch (IOException e)
                {
                    Debug.WriteLine(e.Message);
                    return false;
                }
            }
        }

        /// <summary>
        /// Open XML file with target space data
        /// </summary>
        /// <param name="fileName">Name of file (include path)</param>
        /// <returns>True on success, else fail</returns>
        public override bool Open(string fileName)
        {
            FileName = fileName;
            return Open();
        }

        /// <summary>
        /// Saves the TargetSpace as an XML file.
        /// </summary>
        public override bool Save()
        {
            using (StreamWriter file = new StreamWriter(FileName))
            {
                try
                {
                    file.WriteLine(string.Join(",", VariableNames));
                    for (int i = 0; i < Data.RowCount; i++)
                    {
                        file.WriteLine(string.Join(",", Data.Row(i)));
                    }
                    return true;
                }
                catch(IOException e)
                {
                    Debug.WriteLine(e.Message);
                    return false;
                }
            }
        }

        /// <summary>
        /// Saves the TargetSpace as an XML file.
        /// </summary>
        /// <param name="fileName">Name of file with path</param>
        /// <returns>True on success, else fail</returns>
        public override bool Save(string fileName)
        {
            FileName = fileName;
            return Save();
        }
    }

    /// <summary>
    /// Mahalanobis-Taguchi System class; provides MTS functions
    /// </summary>
    public class MTS
    {
        private TargetSpace targetSpace;

        /// <summary>
        /// Constructor
        /// </summary>
        public MTS() { }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="newSpace">Default target space</param>
        public MTS(TargetSpace newSpace)
        {
            targetSpace = newSpace;
        }

        /// <summary>
        /// Sets base mahalanobis space being compared to
        /// </summary>
        /// <param name="newSpace"></param>
        public void SetSpace(TargetSpace newSpace)
        {
            targetSpace = newSpace;
        }

        /// <summary>
        /// Returns base mahalanobis space being compared to
        /// </summary>
        /// <returns></returns>
        public TargetSpace GetSpace()
        {
            return targetSpace;
        }

        /// <summary>
        /// Calculates Mahalanobis Distance for the selected sample
        /// </summary>
        /// <param name="selectedSample">Sample to be calculated against the target space</param>
        /// <returns>Mahalanobis Distance</returns>
        public double GetMD(Sample selectedSample)
        {
            return GetMD(selectedSample, targetSpace);
        }

        /// <summary>
        /// Calculates Mahalanobis Distance for the selected sample
        /// </summary>
        /// <param name="selectedSample">Sample to be calculated against the target space</param>
        /// <param name="space">TargetSpace to compare sample to</param>
        /// <returns>Mahalanobis Distance</returns>
        public double GetMD(Sample selectedSample, TargetSpace space)
        {
            Debug.WriteLineIf(space.Data.RowCount <= space.Data.ColumnCount, "Error: MTS requires at least N+1 samples for N variables. Continuing...");
            DenseVector Z = new DenseVector(selectedSample.Data.Count);
            for (int i = 0; i < Z.Count; i++)
            {
                Z[i] = (selectedSample.Data[i] - space.GetMeans()[i]) / space.GetStdDevs()[i];
            }
            DenseMatrix inverseC = (DenseMatrix)space.GetCorrelationMatrix().Inverse();
            DenseMatrix transposeZ = (DenseMatrix)Z.ToColumnMatrix();
            DenseVector MD = (Z * inverseC * transposeZ) / space.Data.RowCount;
            return 2 * MD[0];
        }

        /// <summary>
        /// Compares sample to set target space, and provides list of useful variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <returns>List of indeces of useful variables</returns>
        public List<int> Analyze(Sample sampleData)
        {
            TargetSpace temp = new TargetSpace();
            temp.Data = (DenseMatrix)sampleData.Data.ToRowMatrix();
            return Analyze(temp, targetSpace, true);
        }

        /// <summary>
        /// Compares sample to set target space, and provides list of useful or useless variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="useful">True to return useful variable indeces; false to return useless indeces</param>
        /// <returns>List of indeces of useful or useless variables</returns>
        public List<int> Analyze(Sample sampleData, bool useful)
        {
            TargetSpace temp = new TargetSpace();
            temp.Data = (DenseMatrix)sampleData.Data.ToRowMatrix();
            return Analyze(temp, targetSpace, useful);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="space">TargetSpace to be compared with</param>
        /// <returns>List of indeces of useful variables</returns>
        public List<int> Analyze(Sample sampleData, TargetSpace space)
        {
            TargetSpace temp = new TargetSpace();
            temp.Data = (DenseMatrix)sampleData.Data.ToRowMatrix();
            return Analyze(temp, space, true);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="space">TargetSpace to be compared with</param>
        /// <param name="useful">True to return useful variable indeces; false to return useless indeces</param>
        /// <returns>List of indeces of useful variables</returns>
        public List<int> Analyze(Sample sampleData, TargetSpace space, bool useful)
        {
            TargetSpace temp = new TargetSpace();
            temp.Data = (DenseMatrix)sampleData.Data.ToRowMatrix();
            return Analyze(temp, space, useful);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <returns>List of indeces of useful variables</returns>
        public List<int> Analyze(TargetSpace sampleData)
        {
            return Analyze(sampleData, targetSpace, true);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful or useless variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="useful">True to return useful variable indeces; false to return useless indeces</param>
        /// <returns>List of indeces of useful or useless variables</returns>
        public List<int> Analyze(TargetSpace sampleData, bool useful)
        {
            return Analyze(sampleData, targetSpace, useful);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="space">TargetSpace to be compared with</param>
        /// <returns>List of indeces of useful variables</returns>
        public List<int> Analyze(TargetSpace sampleData, TargetSpace space)
        {
            return Analyze(sampleData, targetSpace, true);
        }

        /// <summary>
        /// Compares sample to supplied target space, and provides list of useful or useless variable indeces
        /// </summary>
        /// <param name="sampleData">Sample to be analyzed</param>
        /// <param name="space">TargetSpace to be compared with</param>
        /// <param name="useful">True to return useful variable indeces; false to return useless indeces</param>
        /// <returns>List of indeces of useful or useless variables</returns>
        public List<int> Analyze(TargetSpace sampleData, TargetSpace space, bool useful)
        {
            if (sampleData.Data.ColumnCount != space.Data.ColumnCount) return null;

            List<int> returnList = new List<int>();

            MTS mts = new MTS();
            DenseMatrix OA = GetOrthogonalArray(sampleData.Data.ColumnCount);
            DenseMatrix results = new DenseMatrix(4, sampleData.Data.ColumnCount);
            /* results columns:
             * 0 - number of times variable was used
             * 1 - total signal-to-noise when variable was used
             * 2 - number of times variable was not used
             * 3 - total signal-to-noise when variable not used     */

            for (int run = 0; run < OA.RowCount; run++) // runs on the orthogonal array
            {
                TargetSpace tempSpace = new TargetSpace();
                tempSpace.Data = space.Data;

                TargetSpace tempSample = new TargetSpace();
                tempSample.Data = sampleData.Data;

                List<int> notUsed = new List<int>();
                for (int column = 0; column < tempSpace.Data.ColumnCount; column++) // find out which variables to use on this run
                {
                    if (OA[run, column] != 1)
                        notUsed.Add(column);
                }
                
                // remove the variables that aren't used in this run
                int errorCol = tempSpace.RemoveVariables(notUsed);
                if (errorCol != -1)
                {
                    return null;
                }
                errorCol = tempSample.RemoveVariables(notUsed);
                if (errorCol != -1)
                {
                    return null;
                }

                // calculate signal-to-noise
                double signal_to_noise = 0;
                for(int s = 0; s < sampleData.Data.RowCount; s++)
                {
                    signal_to_noise += 1 / mts.GetMD(tempSample.GetSample(s), tempSpace);
                }
                signal_to_noise = -10 * Math.Log(signal_to_noise / sampleData.Data.RowCount);

                for (int column = 0; column < results.ColumnCount; column++) // record results
                {
                    if (OA[run, column] == 1) // variable was used
                    {
                        results[0, column]++;
                        results[1, column] += signal_to_noise;
                    }
                    else // variable was not used
                    {
                        results[2, column]++;
                        results[3, column] += signal_to_noise;
                    }
                }
            }

            // calculate gains. If positive, variable is useful
            for (int column = 0; column < results.ColumnCount; column++)
            {
                if (useful)
                {
                    if (results[1, column] / results[0, column] - results[3, column] / results[2, column] >= 0) returnList.Add(column);
                }
                else
                {
                    if (results[1, column] / results[0, column] - results[3, column] / results[2, column] < 0) returnList.Add(column);
                }
            }

            return returnList;
        }

        /// <summary>
        /// Generates an orthogonal array.
        /// </summary>
        /// <param name="varCount">Number of variables to be tested.</param>
        /// <returns>DenseMatrix orthogonal array of degree large enough for varCount variables. '1' = use; '2' = do not use</returns>
        public DenseMatrix GetOrthogonalArray(int varCount)
        {
            int runs = MathNet.Numerics.NumberTheory.IntegerTheory.CeilingToPowerOfTwo(varCount + 1);
            if (varCount >= 8 && varCount < 12) // L12 OAs are weird
            {
                double[,] L12 = { {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
                    {1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2},
                    {1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2},
                    {1, 2, 1, 2, 2, 1, 2, 2, 1, 1, 2},
                    {1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 1},
                    {1, 2, 2, 2, 1, 2, 2, 1, 2, 1, 1},
                    {2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 1},
                    {2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 2},
                    {2, 1, 1, 2, 2, 2, 1, 2, 2, 1, 1},
                    {2, 2, 2, 1, 1, 1, 1, 2, 2, 1, 2},
                    {2, 2, 1, 2, 1, 2, 1, 1, 1, 2, 2},
                    {2, 2, 1, 1, 2, 1, 2, 1, 2, 2, 1} };
                return DenseMatrix.OfArray(L12);
            }
            DenseMatrix OA = new DenseMatrix(runs);

            // Power-of-two columns (1, 2, 4, etc.) start at 0 and toggle between 1 and 0 every runs/(2 * column number) rows.
            // Each other column is a binary addition of the power-of-two columns that add up to it ([n,3] = [n,1] + [n,2]).
            for (int column = 1; column < runs; column++)
            {
                if (MathNet.Numerics.NumberTheory.IntegerTheory.IsPowerOfTwo(column)) // power of two column
                {
                    int value = 1;

                    for (int row = 0; row < OA.RowCount; row++)
                    {
                        if (row % (runs / (2 * column)) == 0) value ^= 1;
                        OA[row, column] = value;
                    }
                }
                else // column not a power of two
                {
                    for (int row = 0; row < OA.RowCount; row++)
                    {
                        for (int digit = 0; MathNet.Numerics.NumberTheory.IntegerTheory.PowerOfTwo(digit) < column; digit++) // for each power of two
                        {
                            int digitColumn = MathNet.Numerics.NumberTheory.IntegerTheory.PowerOfTwo(digit);
                            if ((column & digitColumn) == digitColumn)
                            {
                                OA[row, column] = (OA[row, column] + OA[row, digitColumn]) % 2;
                            }
                        }
                    }
                }
            }

            // remove column 0
            OA = (DenseMatrix)OA.SubMatrix(0, OA.RowCount, 1, OA.ColumnCount - 1);
            
            for (int row = 0; row < OA.RowCount; row++)
            {
                for (int column = 0; column < OA.ColumnCount; column++)
                {
                    OA[row, column]++;
                }
            }

            return OA;
        }
    }
}

